feat: Added query-driven sync to PowerSync package#1356
Draft
Chriztiaan wants to merge 4 commits intoTanStack:mainfrom
Draft
feat: Added query-driven sync to PowerSync package#1356Chriztiaan wants to merge 4 commits intoTanStack:mainfrom
Chriztiaan wants to merge 4 commits intoTanStack:mainfrom
Conversation
🦋 Changeset detectedLatest commit: dbe5166 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🎯 Changes
This PR adds query-driven sync and onLoad/onLoadSubset hooks to the PowerSync collection.
1. On-demand Sync mode (blue area)
Added
on-demandsync mode for collections, building on top of the existing eager implementation.Instead of copying the entire source table into the collection upfront,
on-demandmode only syncs the subset of data relevant to active live queries. We achieve this by implementing theloadSubsetandunloadSubsethandlers that TanstackDB calls when live queries are registered or deregistered.1.1 How it works:
1.1.1 Summary
When
loadSubsetis called, we receive the query's where expression from the TanstackDB query API. We compile this down to a SQLiteWHEREclause (taking a comparable approach to what Electric does for PostgreSQL), and the PoC covers every where expression supported by the TanstackDB query API. The compiled expression is added to our set of tracked expressions, and we refresh the diff trigger with all accumulated expressions OR'd together.unloadSubsetremoves the expression and refreshes the diff trigger accordingly.The existing diff trigger and tracking table infrastructure is reused - the only difference is that the trigger now watches a constrained dataset defined by the combined query expressions rather than the full source table.
1.1.2 Stale data eviction on unload
Since where expressions are OR'd together, adding queries only ever widens the synced dataset. When a query is deregistered, however, its data may become stale since it's no longer actively synced. To handle this,
unloadSubsetevicts entries from the collection that match the departing query but not any of the remaining queries, effectively:SELECT id FROM ${viewName} WHERE (${departingWhereSQL}) AND NOT (${remainingWhereSQL}).1.2 Examples
To better understand the advantage of on-demand over eager mode consider the following examples.
1.2.1 Eager mode
1.2.2 On-demand mode
2. Incorporating Sync Streams with on load hooks (red area)
Ideally we would be able to map TanstackDB queries to sync streams automatically, if we can optimise the amount of data sync to
the sqlite database from the service we have smaller set of data that needs to be considered when syncing from the sqlite database to TanstackDB collections.
As a stepping stone towards that, we now expose data loading hooks for both eager and on-demand sync modes that allow a user to call sync streams when a collection is defined (eager mode) or when a collection's data boundary changes based on the live queries predicates (on-demand).
For the these examples we are assuming the follow sync stream exists:
2.1 Eager mode basic usage
Use the
onLoadhook to subscribe to a Sync Stream when the collection first loads, so SQLite is populated before the collection starts serving queries. Ineagermode, all rows in SQLite are synced into the TanStack DB collection; live query filtering then runs against that full dataset.The hook can optionally return a cleanup function where you can unsubscribe from the Sync Stream.
Consider the diagram as an example.

We start with 4 todos in the PowerSync Service, only 2 todos get synced via the sync stream to the SQLite database. Because it's eager mode, both get synced from the SQLite database to the collection. Finally the TanstackDB query only returns the single todo that matches the live query predicate.
Collection
Live Query
A live query that filters by the
completedstate.2.2 On-demand basic usage
Use the
onLoadSubsethook to subscribe to a Sync Stream whenever the collection's data boundary changes (i.e. when the set of active live queries changes). Inon-demandmode, only rows that satisfy the active live query predicates are synced from SQLite into the TanStack DB collection.The hook can optionally return a cleanup function where you can unsubscribe from the Sync Stream when that subset is no longer needed.
Consider the diagram as an example.

We start with 4 todos in the PowerSync Service, only 2 todos get synced via the sync stream to the SQLite database. Because it's on-demand mode, only 1 todo matches gets synced from the SQLite database to the collection. Finally the TanstackDB query only returns the single todo that matches the live query predicate.
Collection
Live Query
A live query that filters by the
completedstate.2.3 Extract a single filter value using
extractSimpleComparisonsGiven a live query like:
onLoadSubsetreceives options.where as an expression treefor eq(list_id, '<uuid>').We parse it to get the
list_idvalue and pass it tosyncStream.Consider the diagram as an example. Note it differs from example 1 and 2 as it aims to illustrate

extractSimpleComparisons.We start with 4 todos in the PowerSync Service, the sync stream subscription criteria (
list_id = "list_1") is derived from the live query registered against the collection. Only 2 todos get synced via the sync stream to the SQLite database. Two todos get synced from the SQLite database to the collection. Finally the TanstackDB query returns both todos as they both matcheq(todo.list_id, 'list_id').Collection
Live Query
Simple filter -> triggers
onLoadSubsetwitheq(list_id, '...')2.4 Use
parseWhereExpressionwith custom handlersparseWhereExpressiongives you full control over how each operator is handled.Here we build a params object for syncStream from the expression tree.
Assume a small adjustment to the sync stream definition of todos (adding the
completedsubscription parameter)Note: We keep the
listparameter name as is (consistent with most of our examples), but to correctly work with the following example we need to map it tolist_id. You may opt to name it aslist_idin the sync stream definition and skip the mapping process.Consider the diagram as an example.

We start with 4 todos in the PowerSync Service, the sync stream subscription criteria (
list_id = "list_1" and completed = 1) is derived from the live query registered against the collection. Only 1 todo gets synced via the sync stream to the SQLite database. One todos gets synced from the SQLite database to the collection. Finally the TanstackDB query returns 1 todo that matcheseq(todo.list_id, 'list_id') and eq(todo.completed, 1).Collection
Live Query
Compound filter -> triggers
onLoadSubsetwithand(eq(list_id, '...'), eq(completed, 1))✅ Checklist
pnpm test.🚀 Release Impact